package org.calzz.jajb.converter;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.calzz.jajb.JsonDeserializer;
import org.calzz.jajb.JsonSerializer;
import org.calzz.jajb.annotation.Converter;
import org.calzz.jajb.annotation.JsonElement;
import org.calzz.jajb.io.IWriter;
import org.springframework.core.MethodParameter;

/**
 * <code>BeanConverter</code> is a converter which handles complex objects for
 * which it was impossible to find a different converter. By using refection
 * mechanisms it retrieves information about a given bean and produces a correct
 * output.
 * @author Marek Brodziak <marek.brodziak@gmail.com>
 * @author Piotr Pejas <piotr.pejas@gmail.com>
 * @since Feb 6, 2008
 */
public class BeanConverter implements IConverter {
	private Log log = LogFactory.getLog(BeanConverter.class);
	private Map<Class<? extends IConverter>, IConverter> _converterCache = new HashMap<Class<? extends IConverter>, IConverter>();

	/*
	 * (non-Javadoc)
	 * @see org.jamjam.jajb.converter.IConverter#canConvert(java.lang.Class)
	 */
	public boolean canConvert(Class<?> clazz) {
		return true;
	}

	/*
	 * (non-Javadoc)
	 * @see org.jamjam.jajb.converter.IConverter#marshal(java.lang.Object,
	 *      java.lang.String, org.jamjam.jajb.io.IWriter,
	 *      org.jamjam.jajb.JsonSerializer)
	 */
	public void marshal(Object obj, String name, IWriter writer, JsonSerializer serializer) {
		writer.startObject(name);

		BeanInfo beanInfo = null;
		try {
			beanInfo = Introspector.getBeanInfo(obj.getClass());
		} catch (IntrospectionException e) {
			writer.endObject();
			log.error(e);
			return;
		}
		PropertyDescriptor props[] = beanInfo.getPropertyDescriptors();

		for (PropertyDescriptor p : props) {

			if ("class".equals(p.getName()) || "declaringClass".equals(p.getName())) {
				continue;
			}
			Object value = null;
			try {
				Method method = p.getReadMethod();
				if (method == null) {
					continue;
				} else {
					value = method.invoke(obj, new Object[] {});
				}
			} catch (Exception e) {
				log.warn(e);
			}

			String beanName = p.getReadMethod().getAnnotation(JsonElement.class) == null ? p.getName() : p.getReadMethod().getAnnotation(JsonElement.class).value();

			if (p.getReadMethod().getAnnotation(Converter.class) == null) {
				serializer.process(value, beanName, writer);
			} else {
				try {
					if (value == null) {
						if (serializer.isSerializeNullValues()) {
							writer.writeKey(beanName);
							writer.writeValue(JsonSerializer.NULL);
						}
					} else {
						IConverter custConverter = fetchUserConverter(p.getReadMethod().getAnnotation(Converter.class).value());
						custConverter.marshal(value, beanName, writer, serializer);
					}
				} catch (Exception e) {
					log.warn(e);
				}
			}
		}

		writer.endObject();
	}

	/*
	 * (non-Javadoc)
	 * @see org.jamjam.jajb.converter.IConverter#unmarshal(java.lang.Object,
	 *      java.lang.Class, org.jamjam.jajb.JsonDeserializer)
	 */
	public <T> Object unmarshal(Object env, Class<T> clazz, JsonDeserializer deserializer, MethodParameter methodParam) {

		Object obj = null;

		try {
			Field[] fields = clazz.getDeclaredFields();
			for (Field f : fields) {
				if ("this$0".equals(f.getName())) {
					log.warn("Inner class not supported: " + clazz.getName());
					return null;
				}
			}
		} catch (Exception e) {
			log.warn(e);
			return null;
		}

		try {
			obj = clazz.newInstance();
		} catch (Exception e) {
			log.warn(e);
			return null;
		}

		BeanInfo beanInfo = null;
		try {
			beanInfo = Introspector.getBeanInfo(clazz);
		} catch (IntrospectionException e) {
			log.error(e);
			return null;
		}

		Map<String, PropertyDescriptor> properties = new HashMap<String, PropertyDescriptor>();
		for (PropertyDescriptor p : beanInfo.getPropertyDescriptors()) {
			if (p.getReadMethod() == null) {
				continue;
			}

			String beanName = p.getReadMethod().getAnnotation(JsonElement.class) == null ? p.getName() : p.getReadMethod().getAnnotation(JsonElement.class).value();
			properties.put(beanName, p);
		}

		Map<?, ?> tokens = (Map<?, ?>) env;
		for (Object token : tokens.keySet()) {
			if (properties.containsKey(token)) {
				PropertyDescriptor p = properties.get(token);
				Method writeMethod = p.getWriteMethod();
				if (writeMethod == null) {
					continue;
				} else {
					try {
						if (p.getReadMethod().getAnnotation(Converter.class) == null) {
							writeMethod.invoke(obj, deserializer.process(tokens.get(token), p.getPropertyType(), new MethodParameter(writeMethod, 0)));
						} else {
							IConverter custConverter = fetchUserConverter(p.getReadMethod().getAnnotation(Converter.class).value());
							writeMethod.invoke(obj, custConverter.unmarshal(tokens.get(token), p.getPropertyType(), deserializer, new MethodParameter(writeMethod, 0)));
						}
					} catch (Exception e) {
						log.warn(e);
					}
				}
			}
		}

		return obj;
	}

	/**
	 * @param clazz
	 * @return
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 */
	private IConverter fetchUserConverter(Class<? extends IConverter> clazz) throws InstantiationException, IllegalAccessException {
		IConverter converterInstance = _converterCache.get(clazz);
		if (converterInstance == null) {
			converterInstance = clazz.newInstance();
			_converterCache.put(clazz, converterInstance);
		}
		return converterInstance;
	}

}
